/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Globalization;
using System.IO;
using VSLangProj;
using VSLangProj80;
using DaveSexton.DocProject.Resources;

namespace DaveSexton.DocProject
{
	/// <threadsafety static="true"/>
	public static class ReferenceResolver
	{
		#region Private / Protected
		private static AppDomain domain;
		#endregion

		#region Methods
		private static void EnsureDomain()
		{
			if (domain == null)
			{
				domain = AppDomain.CreateDomain("DocProject ReferenceResolver");
				domain.UnhandledException += domain_UnhandledException;

				System.Diagnostics.Debug.WriteLine("AppDomain created.", "ReferenceResolver");
			}
		}

		public static IEnumerable<SystemPath> FindUniqueAssemblies(SystemPath path)
		{
			foreach (SystemPath file in path.GetFiles())
				if (file.IsAssembly)
					yield return file;
		}

		public static ICollection<SystemPath> FindUniqueAssemblies(string basePath, ICollection<string> sources)
		{
			if (sources == null)
				throw new ArgumentNullException("sources");

			Dictionary<string, SystemPath> assemblies = new Dictionary<string, SystemPath>(sources.Count, StringComparer.OrdinalIgnoreCase);
			
			foreach (string source in sources)
			{
				if (source == null)
					continue;

				SystemPath path = new SystemPath(basePath, source, true);

				foreach (SystemPath assembly in FindUniqueAssemblies(path))
				{
					AssemblyName name;
					if (assembly.TryGetAssemblyName(out name))
					{
						string key = name.ToString();

						if (!assemblies.ContainsKey(key))
							assemblies.Add(key, assembly);
					}
				}
			}

			return assemblies.Values;
		}

		/// <remarks>
		/// The iterator pattern (i.e., the yield statement in C#) cannot be used to improve the performance of this method because 
		/// the state machine that the C# compiler generates is not serializable and does not derive from <see cref="MarshalByRefObject"/>, 
		/// which prevents the yield statement from crossing an <see cref="AppDomain"/> boundary.  Therefore, a collection of results must 
		/// be generated anyway.
		/// </remarks>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static ICollection<SystemPath> FindUniqueReferencesRecursive(string basePath, ICollection<string> sources)
		{
			return FindUniqueReferencesRecursive(FindUniqueAssemblies(basePath, sources));
		}

		/// <remarks>
		/// The iterator pattern (i.e., the yield statement in C#) cannot be used to improve the performance of this method because 
		/// the state machine that the C# compiler generates is not serializable and does not derive from <see cref="MarshalByRefObject"/>, 
		/// which prevents the yield statement from crossing an <see cref="AppDomain"/> boundary.  Therefore, a collection of results must 
		/// be generated anyway.
		/// </remarks>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		public static ICollection<SystemPath> FindUniqueReferencesRecursive(ICollection<SystemPath> sources)
		{
			if (sources == null)
				throw new ArgumentNullException("sources");

			System.Diagnostics.Debug.WriteLine("Entered FindUniqueReferencesRecursive(sources)", "ReferenceResolver");

			try
			{
				System.Diagnostics.Debug.WriteLine("Preparing to create AppDomain...", "ReferenceResolver");

				// References of source references are resolved by loading the assembly for reflection.
				// Since this code will run in Visual Studio's AppDomain, another AppDomain is used
				// instead to perform the deep recursion so there are no loading conflicts
				EnsureDomain();

				domain.SetData("sources", sources);

				domain.DoCallBack(FindUniqueReferencesRecursiveExternal);

				System.Diagnostics.Debug.WriteLine("AppDomain callback complete.", "ReferenceResolver");

				Dictionary<string, SystemPath> results = (Dictionary<string, SystemPath>) domain.GetData("results");

				return results.Values;
			}
			finally
			{
				if (domain != null)
				{
					System.Diagnostics.Debug.WriteLine("Unloading AppDomain...", "ReferenceResolver");

					try
					{
						AppDomain.Unload(domain);
					}
					// Occurred while testing: Several different projects were building simultaneously, with the debugger attached, while stepping through code (i.e., breakpoint hit for quite some time).
					// Although, this class is thread-safe so there couldn't have been more than one AppDomain created and one reference resolution process occurring at any given time.
					catch (AppDomainUnloadedException) { }

					domain = null;

					System.Diagnostics.Debug.WriteLine("AppDomain unloaded.", "ReferenceResolver");
				}
			}
		}

		private static void FindUniqueReferencesRecursiveExternal()
		{
			ICollection<SystemPath> sources = (ICollection<SystemPath>) AppDomain.CurrentDomain.GetData("sources");
			Dictionary<string, SystemPath> results = new Dictionary<string, SystemPath>(sources.Count, StringComparer.OrdinalIgnoreCase);

			Predicate<SystemPath> predicate = delegate(SystemPath reference)
			{
				string name = reference.AssemblyName.ToString();

				bool include = !results.ContainsKey(name) && !sources.Contains(reference);

				if (include)
					System.Diagnostics.Debug.WriteLine("Dependency: " + name + ", " + reference.Path, "ReferenceResolver");

				return include;
			};

			foreach (SystemPath source in sources)
			{
				if (source == SystemPath.Empty)
					continue;

				if (source.IsDirectory || source.HasWildcard)
				{
					foreach (SystemPath assembly in FindUniqueAssemblies(source))
						foreach (SystemPath path in FindUniqueAssemblyReferencesRecursive(true, assembly, predicate))
							results.Add(path.AssemblyName.ToString(), path);
				}
				else
					foreach (SystemPath path in FindUniqueAssemblyReferencesRecursive(true, source, predicate))
						results.Add(path.AssemblyName.ToString(), path);
			}

			AppDomain.CurrentDomain.SetData("results", results);
		}

		private static IEnumerable<SystemPath> FindUniqueAssemblyReferencesRecursive(bool isDocumentationSource, SystemPath source, Predicate<SystemPath> predicate)
		{
			AssemblyName name;
			if (!source.TryGetAssemblyName(out name))
				yield break;

			Assembly assembly = GetAssemblyFrom(source.Path);

			if (assembly == null &&
				// Normally, if an assembly's references have already been resolved then GetAssemblyFrom will return null since the assembly has already
				// been loaded in the current AppDomain.  But if the source is being documented then the assembly must be inspected even if it has already 
				// been loaded.  This code fixes a bug whereby a project's dependencies would not be discovered if another source project references it and 
				// has been examined first.  An assembly being documented will have already been loaded into the AppDomain only if it's a project reference 
				// to another source that has already been evaluated.  During evaluation of the other source project the current source project itself will 
				// not be examined though since the predicate passed to this method checks to make sure that the reference is not a source project.
				(!isDocumentationSource || (assembly = Assembly.ReflectionOnlyLoad(name.ToString())) == null))
				yield break;

			foreach (AssemblyName assemblyReference in assembly.GetReferencedAssemblies())
			{
				string path = assemblyReference.CodeBase;

				if (string.IsNullOrEmpty(path))
				// CodeBase is usually null for references to the GAC
				{
					Assembly resolvedAssembly = GetReferencedAssembly(assemblyReference, assembly);

					if (resolvedAssembly == null)
					{
						System.Diagnostics.Debug.WriteLine(assemblyReference.ToString(), "ReferenceResolver cannot locate assembly");
						continue;
					}
					else
						path = resolvedAssembly.Location;
				}

				if (!File.Exists(path))
					continue;

				SystemPath reference = new SystemPath(path, assemblyReference);

				if (predicate(reference))
				{
					yield return reference;

					foreach (SystemPath child in FindUniqueAssemblyReferencesRecursive(false, reference, predicate))
						yield return child;
				}
			}
		}

		private static Assembly GetReferencedAssembly(AssemblyName name, Assembly parentAssembly)
		{
			// try the GAC first
			Assembly assembly = GetAssemblyByName(name.FullName);

			if (assembly == null)
			// look in the parent's directory
			{
				Uri uri;

				if (Uri.TryCreate(Path.Combine(Path.GetDirectoryName(parentAssembly.CodeBase), name.Name), UriKind.Absolute, out uri))
					assembly = GetAssemblyFrom(uri.LocalPath);
			}

			return assembly;
		}

		[System.Diagnostics.DebuggerHidden]
		private static Assembly GetAssemblyFrom(string path)
		{
			try
			{
				return Assembly.ReflectionOnlyLoadFrom(path);
			}
			catch (ArgumentException ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}
			catch (BadImageFormatException ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}
			catch (FileNotFoundException ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}
			catch (FileLoadException ex)
			{
				// This exception occurs during an attempt to load an assembly while a different 
				// version has already been loaded into the same AppDomain or a different version 
				// was found first in the configured probing paths (the current AppDomain uses VS's 
				// config file even if it's not explicitly set when the AppDomain is created).
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}

			return null;
		}

		[System.Diagnostics.DebuggerHidden]
		private static Assembly GetAssemblyByName(string fullName)
		{
			try
			{
				return Assembly.ReflectionOnlyLoad(fullName);
			}
			catch (ArgumentException ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}
			catch (BadImageFormatException ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}
			catch (FileNotFoundException ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}
			catch (FileLoadException ex)
			{
				// This exception occurs during an attempt to load an assembly while a different 
				// version has already been loaded into the same AppDomain or a different version 
				// was found first in the configured probing paths (the current AppDomain uses VS's 
				// config file even if it's not explicitly set when the AppDomain is created).
				System.Diagnostics.Debug.WriteLine(ex.ToString(), "ReferenceResolver");
			}

			return null;
		}
		#endregion

		#region Event Handlers
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2201:DoNotRaiseReservedExceptionTypes")]
		private static void domain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			Exception ex = e.ExceptionObject as Exception;

			Log.Exception(ex ?? new Exception((e.ExceptionObject == null) ? Errors.UnknownError : e.ExceptionObject.ToString()),
				Errors.ReferenceResolverAppDomainUnhandledException, e.IsTerminating);
		}
		#endregion
	}
}
